[Py-Intro] Aula 06

Módulos e testes unitários

O que você vai aprender nesta aula?

  • Módulos e scripts
    • Como criar seus próprios módulos
    • Funcionamento da importação de pacotes
    • Como criar módulos executáveis (ou scripts importáveis)
  • Testes
    • O que são testes
    • Etc.

Módulos

Até este momento vimos vários exemplos de código Python, porém todos dentro do Jupyter Notebook. Esta é uma ótima ferramenta para aprendizado, porém ela restringe o uso de nossos programas, estes só são acessíveis de dentro do Jupyter Notebook. A partir de agora vamos começar a criar nossos próprios programas e módulos em arquivos separados que poderão ser reutilizados e separados em vários arquivos conforme a necessidade.

Esses arquivos com código são comumente chamados de scripts e, para o Python, cada arquivo desses é um módulo. Módulos podem ser importado em outros módulos ou no módulo principal (main module).

Um módulo é um arquivo contendo contendo código Python. O nome do arquivo é o nome do módulo com um sufixo .py.

Vamos começar com um exemplo simples, abra um arquivo chamado fibonacci.py e coloque o seguinte código (ou copie o arquivo do repositório na mesma pasta deste notebook):

""" Módulo de números da sequência de Fibonacci """


def fib(n):
    """ Exibe na tela a sequência de Fibonacci até n """
    a, b = 0, 1
    while b < n:
        print(b, end=' ')
        a, b = b, a+b
    print()


def fib2(n):   # return Fibonacci series up to n
    """ Retorna uma lista contendo os números da sequência de Fibonacci até n """
    result = []
    a, b = 0, 1
    while b < n:
        result.append(b)
        a, b = b, a+b
    return result

Na mesma pasta desse arquivo abra o interpretador python com o comando python3.5.

Agora importe o módulo fibonacci da seguinte maneira:


In [2]:
import fibonacci

Acessamos as funções desse módulo:


In [3]:
fibonacci.fib(1000)


1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 

In [4]:
fibonacci.fib2(1000)


Out[4]:
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]

Podemos inspecionar o nome do módulo acessando seu atributo __name__:


In [5]:
fibonacci.__name__


Out[5]:
'fibonacci'

É importante notar que documentamos nosso módulo usando docstrings nas funções e no começo do arquivo. Isso permite que outras ferramentas gerem uma documentação de nosso código como a função help() faz:


In [6]:
help(fibonacci)


Help on module fibonacci:

NAME
    fibonacci - Módulo de números da sequência de Fibonacci

FUNCTIONS
    fib(n)
        Exibe na tela a sequência de Fibonacci até n
    
    fib2(n)
        Retorna uma lista contendo os números da sequência de Fibonacci até n

FILE
    /home/luiz/talks/trilha/01-python-intro/aula-06/fibonacci.py


É dessa forma que as bibliotecas (e o próprio código-fonte da linguagem) são documentados, como podemos constar com os números inteiros:


In [7]:
help(int)


Help on class int in module builtins:

class int(object)
 |  int(x=0) -> integer
 |  int(x, base=10) -> integer
 |  
 |  Convert a number or string to an integer, or return 0 if no arguments
 |  are given.  If x is a number, return x.__int__().  For floating point
 |  numbers, this truncates towards zero.
 |  
 |  If x is not a number or if base is given, then x must be a string,
 |  bytes, or bytearray instance representing an integer literal in the
 |  given base.  The literal can be preceded by '+' or '-' and be surrounded
 |  by whitespace.  The base defaults to 10.  Valid bases are 0 and 2-36.
 |  Base 0 means to interpret the base from the string as an integer literal.
 |  >>> int('0b100', base=0)
 |  4
 |  
 |  Methods defined here:
 |  
 |  __abs__(self, /)
 |      abs(self)
 |  
 |  __add__(self, value, /)
 |      Return self+value.
 |  
 |  __and__(self, value, /)
 |      Return self&value.
 |  
 |  __bool__(self, /)
 |      self != 0
 |  
 |  __ceil__(...)
 |      Ceiling of an Integral returns itself.
 |  
 |  __divmod__(self, value, /)
 |      Return divmod(self, value).
 |  
 |  __eq__(self, value, /)
 |      Return self==value.
 |  
 |  __float__(self, /)
 |      float(self)
 |  
 |  __floor__(...)
 |      Flooring an Integral returns itself.
 |  
 |  __floordiv__(self, value, /)
 |      Return self//value.
 |  
 |  __format__(...)
 |      default object formatter
 |  
 |  __ge__(self, value, /)
 |      Return self>=value.
 |  
 |  __getattribute__(self, name, /)
 |      Return getattr(self, name).
 |  
 |  __getnewargs__(...)
 |  
 |  __gt__(self, value, /)
 |      Return self>value.
 |  
 |  __hash__(self, /)
 |      Return hash(self).
 |  
 |  __index__(self, /)
 |      Return self converted to an integer, if self is suitable for use as an index into a list.
 |  
 |  __int__(self, /)
 |      int(self)
 |  
 |  __invert__(self, /)
 |      ~self
 |  
 |  __le__(self, value, /)
 |      Return self<=value.
 |  
 |  __lshift__(self, value, /)
 |      Return self<<value.
 |  
 |  __lt__(self, value, /)
 |      Return self<value.
 |  
 |  __mod__(self, value, /)
 |      Return self%value.
 |  
 |  __mul__(self, value, /)
 |      Return self*value.
 |  
 |  __ne__(self, value, /)
 |      Return self!=value.
 |  
 |  __neg__(self, /)
 |      -self
 |  
 |  __new__(*args, **kwargs) from builtins.type
 |      Create and return a new object.  See help(type) for accurate signature.
 |  
 |  __or__(self, value, /)
 |      Return self|value.
 |  
 |  __pos__(self, /)
 |      +self
 |  
 |  __pow__(self, value, mod=None, /)
 |      Return pow(self, value, mod).
 |  
 |  __radd__(self, value, /)
 |      Return value+self.
 |  
 |  __rand__(self, value, /)
 |      Return value&self.
 |  
 |  __rdivmod__(self, value, /)
 |      Return divmod(value, self).
 |  
 |  __repr__(self, /)
 |      Return repr(self).
 |  
 |  __rfloordiv__(self, value, /)
 |      Return value//self.
 |  
 |  __rlshift__(self, value, /)
 |      Return value<<self.
 |  
 |  __rmod__(self, value, /)
 |      Return value%self.
 |  
 |  __rmul__(self, value, /)
 |      Return value*self.
 |  
 |  __ror__(self, value, /)
 |      Return value|self.
 |  
 |  __round__(...)
 |      Rounding an Integral returns itself.
 |      Rounding with an ndigits argument also returns an integer.
 |  
 |  __rpow__(self, value, mod=None, /)
 |      Return pow(value, self, mod).
 |  
 |  __rrshift__(self, value, /)
 |      Return value>>self.
 |  
 |  __rshift__(self, value, /)
 |      Return self>>value.
 |  
 |  __rsub__(self, value, /)
 |      Return value-self.
 |  
 |  __rtruediv__(self, value, /)
 |      Return value/self.
 |  
 |  __rxor__(self, value, /)
 |      Return value^self.
 |  
 |  __sizeof__(...)
 |      Returns size in memory, in bytes
 |  
 |  __str__(self, /)
 |      Return str(self).
 |  
 |  __sub__(self, value, /)
 |      Return self-value.
 |  
 |  __truediv__(self, value, /)
 |      Return self/value.
 |  
 |  __trunc__(...)
 |      Truncating an Integral returns itself.
 |  
 |  __xor__(self, value, /)
 |      Return self^value.
 |  
 |  bit_length(...)
 |      int.bit_length() -> int
 |      
 |      Number of bits necessary to represent self in binary.
 |      >>> bin(37)
 |      '0b100101'
 |      >>> (37).bit_length()
 |      6
 |  
 |  conjugate(...)
 |      Returns self, the complex conjugate of any int.
 |  
 |  from_bytes(...) from builtins.type
 |      int.from_bytes(bytes, byteorder, *, signed=False) -> int
 |      
 |      Return the integer represented by the given array of bytes.
 |      
 |      The bytes argument must be a bytes-like object (e.g. bytes or bytearray).
 |      
 |      The byteorder argument determines the byte order used to represent the
 |      integer.  If byteorder is 'big', the most significant byte is at the
 |      beginning of the byte array.  If byteorder is 'little', the most
 |      significant byte is at the end of the byte array.  To request the native
 |      byte order of the host system, use `sys.byteorder' as the byte order value.
 |      
 |      The signed keyword-only argument indicates whether two's complement is
 |      used to represent the integer.
 |  
 |  to_bytes(...)
 |      int.to_bytes(length, byteorder, *, signed=False) -> bytes
 |      
 |      Return an array of bytes representing an integer.
 |      
 |      The integer is represented using length bytes.  An OverflowError is
 |      raised if the integer is not representable with the given number of
 |      bytes.
 |      
 |      The byteorder argument determines the byte order used to represent the
 |      integer.  If byteorder is 'big', the most significant byte is at the
 |      beginning of the byte array.  If byteorder is 'little', the most
 |      significant byte is at the end of the byte array.  To request the native
 |      byte order of the host system, use `sys.byteorder' as the byte order value.
 |      
 |      The signed keyword-only argument determines whether two's complement is
 |      used to represent the integer.  If signed is False and a negative integer
 |      is given, an OverflowError is raised.
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  denominator
 |      the denominator of a rational number in lowest terms
 |  
 |  imag
 |      the imaginary part of a complex number
 |  
 |  numerator
 |      the numerator of a rational number in lowest terms
 |  
 |  real
 |      the real part of a complex number

Um módulo pode conter código python que inicializa o próprio módulo, além de ter definições de funções e variáveis. Esse código de inicialização é executado somente quando o interpretador Python encontra um import <nome do módulo>. Aqui descobrimos que existem códigos que são executados em tempo de importação e outros (como códigos dentro de funções e métodos) que são rodados em tempo de execução.

Os módulos são isolados entre si. Cada um possui sua própria tabela privada de símbolos (contendo funções, variáveis etc.), portanto ao escrever módulos é possível definir variáveis "globais" sem se preocupar com choques de nomes.

Módulos podem importar outros módulos. Geralmente essas importações são feitas no começo do arquivo. Esses módulos importados são adicionados na tabela de símbolos do módulo que realizou a importação.

É considerado boa prática importar somente as funções que serão utilizadas em seu módulo.

Isso é feito assim:


In [10]:
from fibonacci import fib, fib2

In [11]:
fib(500)


1 1 2 3 5 8 13 21 34 55 89 144 233 377 

In [12]:
fib2(500)


Out[12]:
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377]

Também é possível importar todas as funções usando import *, porém essa prática deixa o código mais ilegível e a maioria dos programadores não a utiliza.


In [13]:
from fibonacci import *

In [14]:
fib(500)


1 1 2 3 5 8 13 21 34 55 89 144 233 377 

In [15]:
fib2(500)


Out[15]:
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377]

Também podemos organizar nossos módulos em pastas. Por exemplo temos uma pasta exemplos junto a essa aula que contém uma implementação de vetor euclidiano no arquivo vetor.py. Esse módulo nos fornece uma classe Vetor que armazena as posições x e y em uma tupla, além das funções soma_vetor() e subtrai_vetor().

Aqui está a implementação desse vetor para consulta:

""" Este módulo oferece um vetor espacial e operações de vetor

Fornece uma classe `Vetor` que armazena as posições x e y em uma `namedtuple`
e funções de soma (soma_vetor) e subtração (subtrai_vetor)
"""


from collections import namedtuple

Vetor = namedtuple('Vetor', ['x', 'y'])


def soma_vetor(v1, v2):
    """
    Retorna a soma dos vetores v1 e v2 (v1 + v2)
    """
    return Vetor(v1.x + v2.x, v1.y + v2.y)


def subtrai_vetor(v1, v2):
    """
    Retorna a subtração dos vetores v1 e v2 (v1 - v2)
    """
    return Vetor(v1.x - v2.x, v1.y - v2.y)

Agora vamos importar o módulo localizado em ./exemplos/vetor.py:


In [31]:
from exemplos import vetor

Como vimos anteriomente temos acesso ao nome do módulo através do atributo __name__ da variável vetor:


In [32]:
vetor.__name__


Out[32]:
'exemplos.vetor'

Vale lembrar que o python busca por módulos na pasta em que o interpretador é rodado, como a pasta exemplos está na pasta que o python (e o jupyter notebook) rodam é possível acessá-lo. O Python também busca módulos em outras pastas, porém esse assunto será tratado daqui a pouco.

Agora que temos acesso ao módulo vetor podemos acessar seus recursos e criar um vetor matemático


In [17]:
v1 = vetor.Vetor(x=1, y=5)
v1


Out[17]:
Vetor(x=1, y=5)

In [18]:
v2 = vetor.Vetor(x=-2, y=3)
v2


Out[18]:
Vetor(x=-2, y=3)

Também podemos usar as funções de soma e subtração de vetor:


In [19]:
vetor.soma_vetor(v1, v2)


Out[19]:
Vetor(x=-1, y=8)

In [20]:
vetor.subtrai_vetor(v1, v2)


Out[20]:
Vetor(x=3, y=2)

Assim como fizemos com o exemplo do fibonacci podemos importar diretamente as funções que vamos usar usando from ... import ...:


In [25]:
from vetor import Vetor, soma_vetor, subtrai_vetor

v1 = Vetor(2, 3)
v1


Out[25]:
Vetor(x=2, y=3)

In [26]:
v2 = Vetor(4, -1)
v2


Out[26]:
Vetor(x=4, y=-1)

In [27]:
soma_vetor(v1, v2)


Out[27]:
Vetor(x=6, y=2)

In [28]:
subtrai_vetor(v1, v2)


Out[28]:
Vetor(x=-2, y=4)

Agora que nós entendemos como criar e usar módulos Python, vamos aprender como executar esses módulos como scripts. Um programa python é executado da seguinte forma:

$ python <nome-do-arquivo>.py <argumentos>

Dessa forma o código do módulo é executado, mas o __name__ recebe "__main__". Se adicionarmos o seguinte código no final do modulo fibonnaci.py:

if __name__ == "__main__":
    import sys
    n = int(sys.argv[1])
    fib(n)

Tornamos nosso módulo um script executável além de um módulo importável, pois o código que trata os argumentos é rodado somente quando o módulo executado como o arquivo principal ("main file"):

$ python fibonnaci.py 50
1 1 2 3 5 8 13 21 34 55 89

O módulo sys (importado no exemplo anterior) fornece funções e variáveis do sistema. No exemplo usamos sys.argv que armazena os argumentos enviados a um script python, armazenando-os como strings em uma lista. Para saber mais sobre esse módulo consulte a documentação oficial

Em alguma aula anterior foi falado sobre a função embutida dir() que retorna uma lista de atributos do objeto fornecido. É possível usar essa função em módulos para saber quais funções e atributos esse oferece:


In [34]:
dir(fibonacci)


Out[34]:
['__builtins__',
 '__cached__',
 '__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 'fib',
 'fib2']

In [35]:
dir(vetor)


Out[35]:
['Vetor',
 '__builtins__',
 '__cached__',
 '__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 'namedtuple',
 'soma_vetor',
 'subtrai_vetor']

Testes

"Teste é o processo de executar um programa ou sistema com a intenção de encontrar erros". (Myers, 1979 - The art of software testing)

Diferentes tipos de teste podem ser utilizados para verificar se um programa se comporta como o especificado. Os testes podem ser classificados em teste de caixa-preta, teste de caixa-branca ou teste baseado em defeito. A técnica do teste é definida pelo tipo de informação utilizada para realizar o teste:

- Técnica caixa-preta: testes baseados na especificação de requisitos do programa. Nenhum conhecimento de como o programa é implementado é requerido.
- Técnica caixa-branca: os testes são baseados na implementação do software.
- Técnica baseada em defeito: os testes são baseados em informações históricas sobre defeitos cometidos frequentemente durante o processo de desenvolvimento de software.

Benefícios de testes:

  • Encontrar erros mais cedo: quanto mais tarde um erro é encontrado mais caro custa o estrago.
  • Simplificar integração
  • Ajuda a criar interfaces melhores

Nesta aula veremos somente com Teste de Unidade (ou teste unitário) que faz parte das técnicas de caixa-branca.

O objetivo do teste unitário é identificar erros de lógica e de programação na menor unidade de programação. A unidade, em Python, podem ser métodos, classes e funções.

doctest

Antes de começar a mexer com testes unitários, vamos implementar testes simples usando o módulo doctest.

O módulo doctest procura por textos que parecem com o shell interativo do Python e executa essas linhas verificando se elas executam exatamente como mostrado. É como se você estivesse digitando no modo interativo e recebendo as respostas corretas.

doctest oferece uma maneira simples e fácil de programar usando TDD (Test Driven Development) com Python para iniciantes na linguagem.

Só para deixar claro o módulo doctest não oferece uma API para testes unitários, ele é utilizado para documentação, testes rasos e para usar TDD de uma maneira simples.

Bem, como diria Linus Torvalds "Falar é barato. Mostra-me o código": vamos aos exemplos de testes com doctest para os módulo fibonacci e vetor vistos anteriormente:

""" Módulo de números da sequência de Fibonacci """


def fib(n):
    """
    Exibe na tela a sequência de Fibonacci até n

    >>> fib(10)
    1 1 2 3 5 8 
    >>> fib(-1)
    <BLANKLINE>
    """
    a, b = 0, 1
    while b < n:
        print(b, end=' ')
        a, b = b, a+b
    print()


def fib2(n):   # return Fibonacci series up to n
    """
    Retorna uma lista contendo os números da sequência de Fibonacci até n

    >>> fib2(10)
    [1, 1, 2, 3, 5, 8]
    >>> fib(-1)
    []
    """
    result = []
    a, b = 0, 1
    while b < n:
        result.append(b)
        a, b = b, a+b
    return result

Para executar os doctests nesse exemplo: grave o código em um arquivo, como por exemplo fibonacci.py, abra um terminal e navegue até sua pasta e rode o seguinte comando:

$ python -m doctest fibonacci.py

Se esse comando não emitir mensagem de erro isso significa que o programa passou nos testes. Caso você queira um relatório completo da execução dos testes pode rodar esse comando no modo verboso da seguinte forma:

$ python -m doctest -v fibonacci.py

Ao rodar o último comando a saída deve ser igual a essa:

Trying:
    fib(10)
Expecting:
    1 1 2 3 5 8 
ok
Trying:
    fib(-1)
Expecting:
    <BLANKLINE>
ok
Trying:
    fib2(10)
Expecting:
    [1, 1, 2, 3, 5, 8]
ok
Trying:
    fib2(-1)
Expecting:
    []
ok
1 items had no tests:
    fibonacci
2 items passed all tests:
   2 tests in fibonacci.fib
   2 tests in fibonacci.fib2
4 tests in 3 items.
4 passed and 0 failed.
Test passed.

Como visto no exemplo, os doctests ficam dentro das docstrings e os códigos que vem após >>> são executados como se esivessem na shell Python e a linha a seguir é a resposta esperada da execução do código.

Quando alguma função é executada e não gera saída e nem retorna valor devemos colocar <BLANKLINE> como resposta.

Nesse exemplo testamos: o funcionamento da função fib() que deve imprimir no console a sequência até o número 10, também foi testado uma entrada inválida (n = -1) que não deve imprimir algo. O mesmo foi feito fib2(), com a diferença que esta retorna uma lista.

Agora vamos ver como ficam os doctests para o módulo vetor, grave o seguinte código em um arquivo vetor.py:

"""
Oferece um vetor matemático e operações de vetor

>>> v = Vetor(x=10, y=5)
>>> v.x == v[0] == 10
True
>>> v.y == v[1] == 5
True
"""

from collections import namedtuple


Vetor = namedtuple('Vetor', ['x', 'y'])


def soma_vetor(v1, v2):
    """
    Retorna um vetor que representa a soma dos vetores v1 e v2 (v1 + v2)

    >>> v1 = Vetor(5, 2)
    >>> v2 = Vetor(2, 7)
    >>> soma_vetor(v1, v2)
    Vetor(x=7, y=9)
    """
    return Vetor(x=v1.x + v2.x, y=v1.y + v2.y)


def subtrai_vetor(v1, v2):
    """
    Retorna um vetor que representa a soma dos vetores v1 e v2 (v1 + v2)

    >>> v1 = Vetor(3, 4)
    >>> v2 = Vetor(1, 5)
    >>> subtrai_vetor(v1, v2)
    Vetor(x=2, y=-1)
    """
    return Vetor(x=v1.x - v2.x, y=v1.y - v2.y)

Para rodar os testes navegue até a pasta do arquivo vetor.py em um terminal e digite:

$ python -m doctest -v vetor.py

O resultado desse comando deve ser igual a esse:

Trying:
    v = Vetor(x=10, y=5)
Expecting nothing
ok
Trying:
    v.x == v[0] == 10
Expecting:
    True
ok
Trying:
    v.y == v[1] == 5
Expecting:
    True
ok
Trying:
    v1 = Vetor(5, 2)
Expecting nothing
ok
Trying:
    v2 = Vetor(2, 7)
Expecting nothing
ok
Trying:
    soma_vetor(v1, v2)
Expecting:
    Vetor(x=7, y=9)
ok
Trying:
    v1 = Vetor(3, 4)
Expecting nothing
ok
Trying:
    v2 = Vetor(1, 5)
Expecting nothing
ok
Trying:
    subtrai_vetor(v1, v2)
Expecting:
    Vetor(x=2, y=-1)
ok
4 items had no tests:
    vetor.Vetor
    vetor.Vetor.x
    vetor.Vetor.y
    vetor.multiplica_vetor
3 items passed all tests:
   3 tests in vetor
   3 tests in vetor.soma_vetor
   3 tests in vetor.subtrai_vetor
9 tests in 7 items.
9 passed and 0 failed.
Test passed.

Nesse exemplo testamos: a criação de um objeto da classe Vetor que deve conter os atributos Vetor.x e Vetor.y e permitir o acesso de seus elementos por índice. Também testamos as funções soma_vetor() e subtrai_vetor() em que criamos dois vetores e os passamos como parametro para as funções.

Com isso terminamos este tópico sobre o módulo doctest - que oferece uma maneira simples de testar e documentar APIs. Caso você queria saber mais confira a documentação oficial.

Agora vamos abordar a biblioteca unittest que é utilizada para implementar testes unitários para aplicações Python.

unittest

O módulo unittest faz parte da biblioteca padrão do Python e oferece uma API de testes de unidade baseada no framework xUnit. Alguns pythonistas dizem que a unittest não é pythônica por seu "sotaque javanês".

Nesta aula veremos somente uma simples introdução à testes unitários com unittest. No curso de django falaremos mais profundamente sobre testes com foco em aplicações web.

Vamos começar implementando os testes unitários do módulo fibonacci, para isso crie um arquivo test_fibonacci.py na mesma pasta em que se encontra o arquivo fibonacci.py.

A função fibonnaci.fib() somente imprime a sequência de fibonacci na tela e é mais complicada de testar, por esse motivo vamos começar pelo teste da fibonnaci.fib2() que retorna uma lista:

from unittest import TestCase

from fibonacci import fib2


class TesteFibonacci(TestCase):
    def testa_fib2_entrada_invalida(self):
        sequencia = fib2(-1)
        self.assertEqual(sequencia, [])

    def testa_fib2(self):
        sequencia = fib2(100)
        self.assertEqual(sequencia, [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89])

Rodamos o teste com o seguinte comando:

$ python -m unittest test_fibonnaci.py

Se nenhum erro aparecer isso quer dizer que os testes passaram! Para obter mais informações sobre o teste é possível rodá-los de forma verbosa:

$ python -m unittest -v test_fibonacci.py

E a saida verbosa deve ser igual a essa:

testa_fib2 (exemplos.test_fibonacci.TesteFibonacci) ... ok
testa_fib2_entrada_invalida (exemplos.test_fibonacci.TesteFibonacci) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

Nesse exemplo importamos a classe TestCase do módulo unittest que nos permite definir uma unidade individual de teste que checa respostas específicas para entradas dadas.

Criamos a classe TesteFibonnaci que herda de unittest.TestCase e, portanto, se torna um caso de teste que será rodado pelo "rodador" de testes (test runner). Nessa classe definimos um método chamado testa_fib2_entrada_invalida() para testar a função fibonacci.fib2() quando chamada com uma entrada inválida. Também definimos um método testa_fib2() para testar a criação de uma sequência de fibonacci até 100.

Você deve ter reparado que os métodos da classe TesteFibonacci recebem o argumento self como parametro. Isso acontece pois todos os métodos de classe no Python recebem a instância (self) de forma explícita. Em Java ou C++ o acesso à instância se dá de forma implícita pelo uso do this.

As linhas que chamam self.assertEqual(a, b) são responsáveis por verificar se a saída esperada das funções testadas correspondem ao código implementado.

Para finalizar vamos escrever os testes de unidade do módulo vetor visto anteriormente. Para isso crie um arquivo chamado test_vetor.py na mesma pasta em que se encontra o arquivo vetor.py.

Vamos começar testando a criação do vetor:

from unittest import TestCase

from vetor import Vetor


class TestaVetor(TestCase):
    def testa_vetor(self):
        v = Vetor(x=1, y=-1)
        self.assertEqual(v.x, 1)
        self.assertEqual(v.y, -1)

Para rodar os testes use o seguinte comando:

$ python -m unittest -v test_vetor.py

A saída desse comando deve ser igual a essa:

testa_vetor (exemplos.test_vetor.TesteVetor) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Nesse teste garantimos a existência e funcionamento da classe Vetor: que deve receber dois argumentos x e y e criar dois atributos Vetor.x e Vetor.y e permitir seu acesso.

Agora vamos testar as funções soma_vetor() e subtrai_vetor():

class TestaVetor(TestCase):
    def testa_vetor(self):
        v = Vetor(x=1, y=-1)
        self.assertEqual(v.x, 1)
        self.assertEqual(v.y, -1)

    def testa_soma(self):
        v1 = Vetor(5, 1)
        v2 = Vetor(0, 3)
        v = soma_vetor(v1, v2)
        self.assertEqual(v, Vetor(5, 4))

    def testa_subtrai(self):
        v1 = Vetor(5, 1)
        v2 = Vetor(2, 3)
        v = subtrai_vetor(v1, v2)
        self.assertEqual(v, Vetor(3, -2))

Nos métodos testa_soma() e testa_subtrai() criamos dois vetores, realizamos as operações e testamos com self.assertEqual() se os resultados das funções correspondem ao esperado.

Ao rodar esses testes com o comando

$ pythoon -m unittest -v test_vetor.py

A seguinte mensagem deve aparecer no terminal:

testa_soma (exemplos.test_vetor.TestaVetor) ... ok
testa_subtrai (exemplos.test_vetor.TestaVetor) ... ok
testa_vetor (exemplos.test_vetor.TestaVetor) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

Assim terminanos a aula, agora você já sabe o básico de testes de unidade em Python e agora pode implementar esses testes para suas aplicações caso julgue necessário.

Fim da aula 06

Extra!

Testando funções que o resultado aparece na saída padrão (stdout) como a fibonacci.fib(). Esse teste trará algumas técnicas mais "avançadas" de Python e é totalmente opcional.

Para testar a função fibonacci.fib() precisamos trocar a saída padrão (que exibe caracteres na tela) para uma stream que tenhamos acesso. A stream padrão está em sys.stdout e podemos utilizar a io.StringIO() uma stream em memória que armazena textos em formato string.

Sabendo disso podemos criar nosso teste:

from io import StringIO
import sys
from unittest import TestCase

from fibonacci import fib


class TestFibonacci(TestCase):
    def test_fib(self):
        original_stdout = sys.stdout  # guardamos a stream padrão
        stream = StringIO()  # criamos outra stream que armazena texto
        sys.stdout = stream  # trocamos a stream padrão pela nossa
        fib(100)  # rodamos nosso teste
        result = stream.getvalue()  # pegamos o resultado da nossa stream
        sys.stdout = original_stdout  # desfazemos a troca da stream padrão
        self.assertEqual(result, '1 1 2 3 5 8 13 21 34 55 89 \n')  # testamos o resultado de `fib(100)`

Coloque essa classe no arquivo test_fibonacci.py - que deve estar na mesma pasta do arquivo fibonacci.py - e rode os testes com o comando:

$ python -m unittest -v test_fibonacci.py

A saída deve ser igual a esta:

test_fib (exemplos.test_fib.TestFibonacci) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Acontece que essa não é a melhor maneira de implementar a captura da stdout. Já vimos anteriormente uma forma de lidar com blocos de códigos que funcionam em contextos específicos (neste caso o contexto é o redirecionamento da saída padrão capturada para uma stream durante a execução da função fib()).

A maneira correta de resolver esse problema seria criar um gerenciador de contexto (visto rapidamente na Aula 05 que fala sobre funções e arquivos). Talvez seja necessário consultar a documentação oficial para entender melhor os recursos utilizados no exemplo a seguir.

from io import StringIO
from contextlib import contextmanager
import sys
import unittest

from fibonacci import fib


@contextmanager
def capture_stdout(stream):
    original_stdout = sys.stdout
    sys.stdout = stream
    # o código até aqui é executado ao entrar no gerenciador de contexto
    yield
    # daqui para baixo é executado ao sair do bloco do gerenciador de contexto
    sys.stdout = original_stdout


class TestFibonacci(unittest.TestCase):
    def test_fib(self):
        stream = StringIO()
        with capture_stdout(stream):
            fib(100)
        result = stream.getvalue()
        self.assertEqual(result, '1 1 2 3 5 8 13 21 34 55 89 \n')

Como podemos ver o código do nosso teste fica muito mais simples utilizando o gerenciador de contexto, pois isolamos a lógica de redirecionamento de saída para o context manager.

Quando fui colocar o link da documentação do módulo contextlib que oferece facilidades relacionadas à gerenciadores de contexto aproveitei para lê-la e acabei encontrando uma função que faz o redirecionamento da saída padrão chamada contextlib.redirect_stdout(). Então a solução ideal é não reinventar a roda e utilizá-la:

from io import StringIO
from contextlib import redirect_stdout
import unittest

from fibonacci import fib


class TestFibonacci(unittest.TestCase):
    def test_fib(self):
        stream = StringIO()
        with redirect_stdout(stream):
            fib(100)
        result = stream.getvalue()
        self.assertEqual(result, '1 1 2 3 5 8 13 21 34 55 89 \n')